package erfgame.core.world.terrain;

import java.awt.Point;
import java.awt.Rectangle;
import java.awt.image.BufferedImage;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.logging.Logger;


import erfgame.core.Point3D;
import erfgame.core.PointTransformation;
import erfgame.core.Rotation;
import erfgame.core.util.RangeUtils;
import erfgame.core.world.World;
import erfgame.core.world.terrain.rotated.RotatedWorld;
import erfgame.core.world.terrain.rotated.TransformedBlock;

public class BlockSurfaceImageFactory {

	@SuppressWarnings("unused")
	private static final Logger log = Logger.getLogger(BlockSurfaceImageFactory.class.getName());
	
	public static final int getPixelY( int y, int z ) {
		return (y >> TerrainConstants.Y_SHIFT) + z;
	}
	
	public static final int[][] DOWN_MOVEMENTS = {
		{1,0},
		{-1,0},
		{0,1}
	};

	public static final int[][] UP_MOVEMENTS = {
		{0,-1},
		{1,0},
		{-1,0}
	};

	public static final int[][] LEFT_MOVEMENTS = {
		{-1,0},
		{0,-1},
		{0,1}
	};

	public static final int[][] RIGHT_MOVEMENTS = {
		{1,0},
		{0,-1},
		{0,1}
	};
	
	public final int buildPixelDepthMap( 
			int pixelWidth, 
			int pixelHeight, 
			PixelDepth[][] result,
			int blockDepth,
			TransformedBlock<byte[]> terrain
	) {
		return buildPixelDepthMap(
				0, 
				0, 
				pixelWidth, 
				pixelHeight, 
				result, 
				blockDepth, 
				terrain
		);
	}
	
	public final int buildPixelDepthMap(
			int pixelX, 
			int pixelY,
			int pixelWidth, 
			int pixelHeight, 
			PixelDepth[][] result,
			int blockDepth,
			TransformedBlock<byte[]> terrain
	) {
		Point tempPoint = new Point();
		int blockHeight = terrain.getHeight();
		int blockHeightDiv2 = blockHeight >> 1;
		int totalPixelHeight = getPixelY(blockHeight, blockDepth); 
		int maxDepths = 0;
		Rotation rotation = terrain.getRotation();
		byte[][] blockGridIds = this.blockGridIds[rotation.ordinal()];
		for( int py=pixelHeight+pixelY; py>pixelY; ) {
			py--;
			int ay = py;
			int min = Math.max( blockHeightDiv2-ay, 0 );
			int max = Math.min( blockHeightDiv2, totalPixelHeight-ay );
			for( int px=pixelWidth+pixelX; px>pixelX; ) {
				px--;
				int ax = px;
				int numDepths = 0;
				int tx = ax;
				boolean inAir = true;
				int firstDepthStart = -1;
				int firstDepthEnd = -1;
				int firstBlockGridId = -1;
				for( int i=min; i<max; i++ ) {
					int ty = blockHeight - ((i+1)<<1);
					int tz = ay + i - blockHeightDiv2;	
					byte terrainType = terrain.getPart( tx , ty, tempPoint )[ tz ];
					if( terrainType == TerrainConstants.TERRAIN_TYPE_AIR ^ inAir ) {
						if( numDepths == 0 ) {
							firstDepthStart = i;
							firstBlockGridId = blockGridIds[tx][ty];
						} else if( firstDepthEnd < 0 ) {
							firstDepthEnd = i;
						}
						numDepths++;
						inAir = !inAir;
					}
				}
				if( !inAir ) {
					numDepths++;
					if( firstDepthEnd < 0 ) {
						firstDepthEnd = max;
					}
				}
				PixelDepth depth;
				if( firstDepthStart >= 0 ) {
					depth = new PixelDepth(
							firstDepthStart, 
							firstDepthEnd, 
							(byte)firstBlockGridId, 
							numDepths
					);
				} else {
					depth = null;
				}
				result[px][py] = depth;
				if( numDepths > maxDepths ) {
					maxDepths = numDepths;
				}
			}
		}
		return maxDepths;
	}
	
	public static final void clear( BufferedImage image, int width, int height ) {
		for( int x=width; x>0; ) {
			x--;
			for( int y=height; y>0; ) {
				y--;
				image.setRGB( x, y, 0x00000000 );
			}
		}
	}
	
	public final ArrayList<BoundsAndGridId> buildSurfaceMap(
			int pixelWidth, 
			int pixelHeight, 
			int[][] result, 
			PixelDepth[][] pixelDepthMap, 
			int startingSurfaceId
	){
		return buildSurfaceMap( 0, 0, pixelWidth, pixelHeight, result, pixelDepthMap, startingSurfaceId );
	}
	
	public final ArrayList<BoundsAndGridId> buildSurfaceMap(
			int pixelX, 
			int pixelY,
			int pixelWidth, 
			int pixelHeight, 
			int[][] result, 
			PixelDepth[][] pixelDepthMap, 
			int startingSurfaceId
	) {
		// TODO : maybe these should be stored internally rather than created each time?
		int[][] points = new int[ pixelWidth * pixelHeight ][2];
		ArrayList<BoundsAndGridId> surfaceBounds = new ArrayList<BoundsAndGridId>();
		int surfaceId = startingSurfaceId;
		for( int x=pixelWidth+pixelX; x>pixelX; ) {
			x--;
			int px = x;
			for( int y=pixelHeight+pixelY; y>pixelY; ) {
				y--;
				int py = y;
				if( result[ px ][ py ] == 0 ) {
					PixelDepth pixelDepth =  pixelDepthMap[px][py]; 
					//int pixelDepth = pixelDepthMap[px][py].length;
					if( pixelDepth != null ) {
						int numDepths = pixelDepth.getNumDepths();
						// TODO : should reduce the bounds to just those passed in above
						BoundsAndGridId bounds = buildSurface( 
								points, 
								pixelX+pixelWidth, 
								pixelY+pixelHeight,
								result, 
								pixelDepthMap, 
								px, 
								py,
								numDepths,
								surfaceId
						);
						surfaceId ++;
						surfaceBounds.add( bounds );
					}
				}
			}
		}
		return surfaceBounds;
	}
	
	private final BoundsAndGridId buildSurface(
			int[][] points, 
			int pixelWidth, 
			int pixelHeight, 
			int[][] result, 
			PixelDepth[][] pixelDepthMap,
			int startx, 
			int starty,
			int surfaceDepth,
			int surfaceId
	) {
		result[startx][starty] = surfaceId;
		//byte blockGridId = pixelDepthMap[startx][starty].getFirstBlockGridId();
		// turn off block grid separation for images (it's a performance hit)
		byte blockGridId = -1; 
		Rectangle bounds = buildSurface(
				points, 
				pixelWidth, 
				pixelHeight, 
				result, 
				pixelDepthMap,
				startx, 
				starty,
				surfaceDepth,
				surfaceId,
				blockGridId
		);
		return new BoundsAndGridId( bounds, blockGridId );
	}
	
	
	private TerrainColorSourceIndex colorSourceIndex;
	private TerrainColorSource unknownColorSource;
	private int maxGap;
//	private int[][][] tempPixelDepthMap;
//	private int[][] tempPixelSurfaceMap;
	private World world;
	private byte[][][] blockGridIds;
	private HorizontalAngleVoxelColorPicker colorPicker;
	private int maxShadow;
	
	public BlockSurfaceImageFactory( World world, TerrainColorSourceIndex colorSourceIndex, TerrainColorSource unknownColorSource, int maxGap ) {
		this.colorSourceIndex = colorSourceIndex;
		this.unknownColorSource = unknownColorSource;
		this.maxGap = maxGap;
		this.maxShadow = world.getMaxShadowDepth();
		this.world = world;
		this.blockGridIds = generateBlockGridIds();
		this.colorPicker = new HorizontalAngleVoxelColorPicker( 
				-Math.PI/8, 
				0, 
				16, 
				unknownColorSource, 
				world.getBlockWidth(), 
				world.getBlockHeight(), 
				world.getBlockDepth(), 
				false 
		);
	}
	
	public TerrainColorSourceIndex getColourSourceIndex() {
		return colorSourceIndex;
	}
	
	private final byte[][][] generateBlockGridIds() {
		Rotation[] rotations = Rotation.values();
		byte[][][] result = new byte[rotations.length][][];
		for( int i=rotations.length; i>0; ) {
			i--;
			result[i] = generateBlockGridIds(rotations[i]);
		}
		return result;
	}
	
	private final byte[][] generateBlockGridIds(Rotation rotation) {
		PointTransformation transformation;
		transformation = rotation.getRotateInBoxTransformation(world.getBlockWidth()-1, world.getBlockHeight()-1);
		Point temp = new Point();
		byte[][] result = new byte[world.getBlockWidth()][world.getBlockHeight()];
		for( int x=world.getBlockWidth(); x>0; ) {
			x--;
			for( int y=world.getBlockHeight(); y>0; ) {
				y--;
				transformation.transform(x, y, temp);
				Point gridCoords = world.getGridCoordinates(temp.x, temp.y);
				int gx = gridCoords.x;
				int gy = gridCoords.y;
				byte gridId = world.getGridId(gx, gy);
				result[x][y] = gridId;
				
			}
		}
		return result;
	}

	private final Rectangle buildSurface(
			int[][] points, 
			int pixelWidth, 
			int pixelHeight, 
			int[][] result, 
			PixelDepth[][] pixelDepthMap,
			int startx, 
			int starty,
			int surfaceDepth,
			int surfaceId, 
			byte blockGridId
	) {
		int minx = startx;
		int maxx = startx;
		int miny = starty;
		int maxy = starty;
		
		points[0][0] = startx;
		points[0][1] = starty;
		int numPoints = 1;
		while( numPoints > 0 ) {
			numPoints--;
			int[] point = points[ numPoints ];
			int px = point[0];
			int py = point[1];
			PixelDepth depths = pixelDepthMap[px][py];
			int start;
			int end;
			start = depths.getFirstDepthStart();
			end = depths.getFirstDepthEnd();
			int[][] movements;
			if( px < startx ) { 
				movements = LEFT_MOVEMENTS;
			} else if( px > startx ) {
				movements = RIGHT_MOVEMENTS;
			} else {
				// only go up, we always start at the bottom
				movements = UP_MOVEMENTS;
			}
			for( int i=movements.length; i>0; ) {
				i--;
				int[] movement = movements[i];
				int dx = movement[0];
				int dy = movement[1];
				int newpx = px + dx;
				int newpy = py + dy;
				if( newpx >= 0 && 
						newpy >= 0 && 
						newpx < pixelWidth &&
						newpy < pixelHeight ) {
					if( result[ newpx ][ newpy ] == 0 ) {
						PixelDepth newDepths = pixelDepthMap[newpx][newpy];
						if( newDepths != null ) {
							int newDepthsBlockGridId = newDepths.getFirstBlockGridId();
							int newDepthsLength = newDepths.getNumDepths();
							if( surfaceDepth == newDepthsLength && (blockGridId < 0 || blockGridId == newDepthsBlockGridId) ) {
								int newStart = newDepths.getFirstDepthStart();
								int newEnd = newDepths.getFirstDepthEnd(); 
								if( RangeUtils.overlaps( start-this.maxGap, end+this.maxGap, newStart-this.maxGap, newEnd+this.maxGap ) ) {
									// check the offsets
									point = points[ numPoints ];
									point[0] = newpx;
									point[1] = newpy;
									numPoints++;
									// mark it as already in queue
									result[newpx][newpy] = surfaceId;
									// TODO : are comparisons faster than min/max (native operations?)
									minx = Math.min( newpx, minx );
									maxx = Math.max( newpx, maxx );
									miny = Math.min( newpy, miny );
									maxy = Math.max( newpy, maxy );
								}
							}
						}
					}
				}
			}
		}
		return new Rectangle( minx, miny, maxx-minx+1, maxy-miny+1 );
	}
	
	public final BlockSurfaceImages render(
			int cx, 
			int cy, 
			int cz,
			TransformedBlock<TransformedBlock<byte[]>[]> terrain, 
			byte[][][][][][] rawTerrain,
			int[][][][][] surfaces,
			Point3D blockWorldPosition,
			RotatedWorld world,
			boolean clearRoof
	) {
		BlockSurfaceImages result = new BlockSurfaceImages();
		TransformedBlock<byte[]> centerTerrain = terrain.getPart(cx, cy)[cz];
		int blockWidth = world.getBlockWidth();
		int blockHeight = world.getBlockHeight();
		int blockDepth = world.getBlockDepth();
		
		int pixelWidth = blockWidth;
		int pixelHeight = getPixelY( blockHeight, blockDepth );
		
		// TODO : this should probably be reused from somewhere
		PixelDepth[][] pixelDepths = new PixelDepth[ pixelWidth ][ pixelHeight ];
		// make the depth map
		buildPixelDepthMap(
				pixelWidth, 
				pixelHeight, 
				pixelDepths, 
				blockDepth,
				centerTerrain
		);
		// TODO : this should probably be reused from somewhere
		int[][] pixelSurfaceMap = new int[ pixelWidth ][ pixelHeight ];
		ArrayList<BoundsAndGridId> surfaceBoundingBoxes = buildSurfaceMap(
				pixelWidth, 
				pixelHeight, 
				pixelSurfaceMap, 
				pixelDepths, 
				1 
		);
		SurfaceImage[] surfaceImages = new SurfaceImage[ surfaceBoundingBoxes.size() ];
		for( int i=surfaceBoundingBoxes.size(); i>0; ) {
			int surfaceId = i;
			i--;
			BoundsAndGridId bag = surfaceBoundingBoxes.get( i );
			Rectangle surfaceBoundingBox = bag.bounds;
			BufferedImage bufferedImage = new BufferedImage( 
					surfaceBoundingBox.width, 
					surfaceBoundingBox.height, 
					BufferedImage.TYPE_INT_ARGB
			);
			SurfaceImage surfaceImage = renderSurfaceImage( 
					surfaceBoundingBox.x, 
					surfaceBoundingBox.x + surfaceBoundingBox.width, 
					surfaceBoundingBox.y, 
					surfaceBoundingBox.y + surfaceBoundingBox.height, 
					pixelDepths, 
					surfaceId, 
					pixelSurfaceMap, 
					surfaces,
					blockWorldPosition.x << world.getBlockWidthShift(), 
					blockWorldPosition.y << world.getBlockHeightShift(), 
					blockWorldPosition.z * blockDepth,
					world,
					cx, cy, cz,
					terrain,
					rawTerrain,
					bufferedImage,
					bag.blockGridId, 
					clearRoof
			);
			surfaceImages[ i ] = surfaceImage;
		}
		result.setSurfaceImages( surfaceImages );
		result.setNextSurfaceId( surfaceImages.length + 1 );
		result.setPixelDepths( pixelDepths );
		result.setSurfaces( pixelSurfaceMap );
		return result;
	}

	public final SurfaceImage renderSurfaceImage(
			int startpx, 
			int endpx, 
			int startpy, 
			int endpy,
			PixelDepth[][] pixelDepths,
			int selectedSurface,
			int[][] surfaceMap,
			int[][][][][] surfaces,
			int blockWorldX, 
			int blockWorldY, 
			int blockWorldZ,
			RotatedWorld world,
			int cx,
			int cy,
			int cz,
			TransformedBlock<TransformedBlock<byte[]>[]> terrain, 
			byte[][][][][][] rawTerrain,
			BufferedImage image,
			byte blockGridId, 
			boolean clearRoof
	) {
		TransformedBlock<byte[]>[] centerTerrainColumn = terrain.getPart(cx, cy); 
		TransformedBlock<byte[]> centerTerrain = centerTerrainColumn[cz];
		
//		int[][] atan2sin = ColorUtils.ATAN2_VALUES[ 0 ];
		
		Point tempPoint = new Point();
		
		Rotation rotation = terrain.getRotation();
		Point adjustment = rotation.rotate( 0, 1 );
		int orientation = rotation.ordinal();
		
		byte[][][] rawCenterTerrain = centerTerrain.getRawData();
		PointTransformation transformation = centerTerrain.getTransformation();
		
		int blockWidth = world.getBlockWidth();
		int blockHeight = world.getBlockHeight();
		int blockHeightDiv2 = blockHeight>>1;
		int blockDepth = world.getBlockDepth();
		
		for( int px = startpx; px<endpx; px++ ) {
			int tx = px;
//			int worldX = blockWorldX + tx;
			for( int py=startpy; py<endpy; py++ ) {
				int surface = surfaceMap[ px ][ py ];
				if( surface == selectedSurface ) {
					PixelDepth depths = pixelDepths[ px ][ py ];
					int depthStart = depths.getFirstDepthStart();
					//int depthEnd = depths[ 1 ];
					int ty = blockHeight - ((depthStart+1)<<1);
					int tz = py + depthStart - blockHeightDiv2;
					// render
					transformation.transform(tx, ty, tempPoint);
					//byte terrainType = centerTerrain.getPart(tx, ty)[tz];
					byte terrainType = rawCenterTerrain[tempPoint.x][tempPoint.y][tz];
					// should never be air
					TerrainColorSource colorSource = colorSourceIndex.getTerrainColorSource( terrainType );
//					int worldY = ty + blockWorldY;
					// TODO need to add in (rotated) block offset (think these are unrotated), however this is probably never actually going to be required (most things mod by the block size)
					int worldX = tempPoint.x + blockWorldX;
					int worldY = tempPoint.y + blockWorldY;
					int worldZ = tz + blockWorldZ;
					
					int adjustedtx;
					int adjustedty;
					int adjustedtz;
					int adjustedcz;
					
					// test the diagonal for blocking
					int testx = tempPoint.x + adjustment.x;
					int testy = tempPoint.y + adjustment.y;
					int testz;
					int testcz;
					byte[][][] testTerrain;
					if( tz > 0 ) {
						testz = tz - 1;
						testcz = cz;
						testTerrain = rawCenterTerrain;
					} else {
						if( cz > 0 && centerTerrainColumn[cz-1] != null ) {
							testz = blockDepth - 1;
							testcz = cz-1;
							testTerrain = centerTerrainColumn[testcz].getRawData();
						} else {
							testz = tz;
							testcz = cz;
							testTerrain = rawCenterTerrain;
						}
					}
					// make the depths factor in the offset takes into account the view angle (to burrow out of any rounding errors)
					if( testTerrain[testx][testy][testz] != TerrainConstants.TERRAIN_TYPE_AIR ) {
						adjustedtx = testx;
						adjustedty = testy;
						adjustedtz = testz;
						adjustedcz = testcz;
					} else {
						// test the horizontal for blocking
						if( rawCenterTerrain[testx][testy][tz] != TerrainConstants.TERRAIN_TYPE_AIR ) {
							adjustedtx = testx;
							adjustedty = testy;
						} else {
							adjustedtx = tempPoint.x;
							adjustedty = tempPoint.y;
						}
						adjustedtz = tz;
						adjustedcz = cz;
					}

//					int[][] offsets = {
//							{1,0,0},
//							{0,0,1},
//							{-1,0,0},
//							{0,0,-1},
//							{0,1,0},
//							{0,-1,0}
//					};
//					boolean boxed = true;
//					for(int i=0; i<offsets.length; i++) {
//						int[] offset = offsets[i];
//						int rx = adjustedtx + offset[0];
//						int ry = adjustedty + offset[1];
//						int rz = adjustedtz + offset[2];
//						if( rx >= 0 && ry >= 0 && rz >= 0 && rx < blockWidth && ry < blockHeight && rz < blockDepth ) {
//							if( testTerrain[rx][ry][rz] == TerrainConstants.TERRAIN_TYPE_AIR ) {
//								boxed = false;
//								break;
//							}
//						}
//					}
//					if( boxed && rotation == Rotation.degrees0 ) {
//						log.warning("boxed in ("+adjustedtx+","+adjustedty+","+adjustedtz+") at "+rotation.getDegrees());
//						StringBuffer sb = new StringBuffer();
//						sb.append("---\n");
//						for( int z=0; z<blockDepth; z++ ) {
//							sb.append(Integer.toString(z));
//							if( z < 10 ) {
//								sb.append(' ');
//							}
//							for( int x=0; x<blockWidth; x++ ) {
//								byte type = rawCenterTerrain[x][adjustedty][z];
//								if( type == TerrainConstants.TERRAIN_TYPE_AIR) {
//									sb.append(' ');
//								} else {
//									sb.append('*');
//								}
//							}
//							sb.append('\n');
//						}
//						sb.append("---\n");
//						for( int z=0; z<blockDepth; z++ ) {
//							sb.append(Integer.toString(z));
//							if( z < 10 ) {
//								sb.append(' ');
//							}
//							for( int y=0; y<blockWidth; y++ ) {
//								byte type = rawCenterTerrain[adjustedtx][y][z];
//								if( type == TerrainConstants.TERRAIN_TYPE_AIR) {
//									sb.append(' ');
//								} else {
//									sb.append('*');
//								}
//							}
//							sb.append('\n');
//						}
//						log.warning(sb.toString());
////						if(adjustedtz > 0) {
////							adjustedtz--;
////						}
//					}
					
					int multiplier = colorPicker.getMultiplier( 
							cx, cy, adjustedcz,
							rawTerrain, 
							adjustedtx, adjustedty, adjustedtz, 
							worldX, 
							worldY, 
							worldZ, 
							clearRoof
					);
					int color;
					if( multiplier != AbstractVoxelColorPicker.INVALID ) {
						int dCeiling;
						if( surfaces != null ) {
							// want to test the next visible pixel to ensure that walls are shadowed correctly
							int shadowbx = cx;
							int shadowby = cy;
							int shadowtx = tempPoint.x + (adjustment.x * 2);
							int shadowty = tempPoint.y + (adjustment.y * 2);
							// TODO can go over the extents of the rendering, probably should make it update the terrain only up until the buffer-2 to allow proper shadows rather than bounds check here
							if( shadowtx < 0 ) {
								if( shadowbx > 0 ) {
									shadowbx--;
									shadowtx += blockWidth;
								} else {
									shadowtx = tempPoint.x;
								}
							} else if( shadowtx >= blockWidth ) {
								if( shadowbx < surfaces.length-1 ) {
									shadowbx++;
									shadowtx -= blockWidth;								
								} else {
									shadowtx = tempPoint.x;
								}
							}
							if( shadowty < 0 ) {
								if( shadowby > 0 ) {
									shadowby--;
									shadowty += blockHeight;
								} else {
									shadowty = tempPoint.y;
								}
							} else if( shadowty >= blockHeight ) {
								if( shadowby < surfaces[shadowbx].length-1 ) {
									shadowby++;
									shadowty -= blockHeight;
								} else {
									shadowty = tempPoint.y;
								}
							}
							
							int[][][] csurfaces = surfaces[shadowbx][shadowby];
							if( csurfaces != null ) {
								int[] xysurfaces = csurfaces[shadowtx][shadowty];
								int ceilingIndex = Arrays.binarySearch(xysurfaces, worldZ);
								if( ceilingIndex < 0 ) {
									ceilingIndex = -(ceilingIndex + 1); 
								} 
								if( (ceilingIndex & 1) != 0 ) {
									ceilingIndex--;
								}
								if( ceilingIndex <= 0 ) {
									dCeiling = -1;
								} else {
									dCeiling = worldZ - xysurfaces[ceilingIndex-1];
								}
							} else {
								dCeiling = -1;
							}
						} else {
							dCeiling = -1;
						}
						color = colorSource.getColor(
								worldX, 
								worldY, 
								worldZ, 
								multiplier, 
								AbstractVoxelColorPicker.PRECISION, 
								dCeiling
						);
					} else {
						color = unknownColorSource.getColor(
								worldX, 
								worldY, 
								worldZ, 
								0, 
								AbstractVoxelColorPicker.PRECISION, 
								-1
						);
					}
//					if( px == startpx || px == endpx-1 || py == startpy || py == endpy-1 ) {
//						color = 0xFFFF0000;
//					}
					//int color = 0xFF000000 | (selectedSurface * 0x111111); 
					// TODO : efficiency?
					// TODO : edges aren't coloured as well as centers, will it be visible?
					image.setRGB( px-startpx, py-startpy, color );
				}
			}
		}
		SurfaceImage result;
		result = new SurfaceImage( 
				startpx,
				startpy,
				selectedSurface,
				image,
				blockGridId
		);
		return result;

	}
	
	public static class BoundsAndGridId {
		public byte blockGridId;
		public Rectangle bounds;
		
		public BoundsAndGridId( Rectangle bounds, byte blockGridId ) {
			this.blockGridId = blockGridId;
			this.bounds = bounds;
		}
	}
}
